/**
 * Return new Table-class entity
 * @param records (option) array of records OR object of records, omit to create blank table
 * @param idAttr (option) attributeName of record's id-attribute, omit to using the member-index
 * @constructor
 */
var table = function(records, idAttr){
    /**
     * Do get/set to records
     * @param id (required)
     * @param record (option), if omit, get id's record; if null, delete id's record; else, set id's record
     */
    var tbl = function (id, record) {
        if (arguments.length === 1)
            return tbl._records[id];
        return tbl.set(id, record);
    };

    tbl._type = 'table';
    tbl._records = {};
    // Table Events : this._events.xxx : table.event(['beforeAdd', 'beforeDel','afterAdd', 'afterDel'], function(id,record,eid,table,...args){})
    // Update Events : this._events.xxx : table.event(['beforeAttr_'+fid3, 'afterAttr_'+fid3, ...], function(id,record,eid,table,...args){})
    tbl._events = {};

    tbl.messageToEvents = function(msg_msgs){
        let eidSet = new Set();
        if (msg_msgs instanceof Array){
            for (let x in msg_msgs){
                if (x !== undefined){
                    let msg = msg_msgs[x];
                    for (let eid in this._events){
                        if (eid !== undefined && !eidSet.has(eid)){
                            if (this._events[eid].msgs.has(msg))
                                eidSet.add(eid);
                        }
                    }
                }
            }
        }else{
            for (let eid in this._events){
                if (eid !== undefined && !eidSet.has(eid)){
                    if (this._events[eid].msgs.has(msg_msgs))
                        eidSet.add(eid);
                }
            }
        }
        let eids = Array.from(eidSet);
        eids.sort((x, y) => x.tOrder - y.tOrder);
        return eids;
    };

    tbl.doEvents = function(msg_msgs, id, record, ...args){
        let eids = this.messageToEvents(msg_msgs);
        eids.sort((x, y) => x.tOrder - y.tOrder);
        for (let i = 0; i < eids.length; i++) {
            let eid = eids[i];
            if (this._events[eids](this, id, record, eid, ...args) === false)
                return false;
        }
        return true;
    };

    tbl.existEvent = function(eid){
        return this._events[eid] !== undefined;
    };

    tbl.getEvent = function(eid){
        return this._events[eid];
    };

    tbl._addEvent = function(eid, event){
        if (event == null || event['_type'] === 'event'){
            this._events[eid] = event;
            return true;
        }
        return false;
    };

    tbl.delEvent = function(eid) {
        if (!this.existEvent(eid))
            return true;
        delete this._events[eid];
        return true;
    };

    tbl.setEvent = function(eid, event){
        if (event === this._events[eid])
            return true;
        if (this.existEvent(eid) && !this.delEvent(eid))
            return false;
        if (typeof (event) !== 'function' || event['_type'] !== 'event')
            return true;
        return this._addEvent(eid, event);
    };

    tbl.gsetEvent = function(eid, event){
        if (event === undefined)
            return this._events[eid];
        return this.setEvent(eid, event);
    };

    tbl.exist = function(id){
        return this._records[id] !== undefined;
    };

    tbl.get = function(id){
        return this._records[id];
    };

    tbl._add = function(id, record){
        if (!this.doEvents('beforeAdd', id, record))
            return false;
        this._records[id] = record;
        if (!this.doEvents('afterAdd', id, record))
            return false;
        return true;
    };

    tbl.del = function(id){
        if (!this.exist(id))
            return true;
        if (!this.doEvents('beforeDel', id, this._records[id]))
            return false;
        delete this._records[id];
        if (!this.doEvents('afterDel', id, this._records[id]))
            return false;
        return true;
    };

    tbl.delAll = function(){
        for (let id in this._records)
            if (id !== undefined)
                this.del(id);
    };

    tbl.set = function(id, record){
        if (record === this._records[id])
            return true;
        if (this.exist(id) && !this.del(id))
            return false;
        if (record === undefined)
            return true;
        return this._add(id, record);
    };

    tbl.setWithoutMessage = function(id, record){
        if (record !== this._records[id]){
            if (this.exist(id) && record === null)
                delete this._records[id];
            else
                this._records[id] = record;
        }
        return true;
    };

    /**
     * Do get/set to records
     * @param id (required)
     * @param record (option), if omit, get id's record; if null, delete id's record; else, set id's record
     */
    tbl.gset = function(id, record){
        return this(id, record);
    };

    tbl.replaceId = function(id, newid){
        if (!this.set(newid, this._records[id]))
            return false;
        return this.del(id);
    };

    tbl.setRecords = function(records, idAttr){
        if (records instanceof Object){
            for (let i in records){
                if (i !== undefined){
                    let record = records[i];
                    let id = idAttr ? record[idAttr] : i;
                    if (id !== undefined) {
                        this.set(id, record);
                    }
                }
            }
        }
    };

    tbl._update = function(id, attrs_attrscb){
        let record = this._records[id];
        if (record == undefined)
            return false;
        let attrs = attrs_attrscb instanceof Function ? attrs_attrscb(this, id) : attrs_attrscb;

        if (! attrs instanceof  Object){
            let bothValueType = ! record instanceof  Object;
            if (bothValueType)
                this._records[id] = attrs;
            return bothValueType;
        }

        let beforeMsgs = [];
        let afterMsgs = [];
        for (let x in attrs){
            if (x !== undefined){
                beforeMsgs.push('beforeAttr_' + x);
                afterMsgs.push('afterAttr_' + x);
            }
        }

        if (!this.doEvents(beforeMsgs, id, record, attrs))
            return false;
        for (let x in attrs){
            if (x !== undefined)
                record[x] = attrs[x];
        }
        if (!this.doEvents(afterMsgs, id, record, attrs))
            return false;
        return true;
    };

    tbl._updateWithoutMessage = function(id, attrs_attrscb){
        let record = this._records[id];
        if (record == undefined)
            return false;
        let attrs = attrs_attrscb instanceof Function ? attrs_attrscb(this, id) : attrs_attrscb;

        if (! attrs instanceof  Object){
            let bothValueType = ! record instanceof  Object;
            if (bothValueType)
                this._records[id] = attrs;
            return bothValueType;
        }
        for (let x in attrs){
            if (x !== undefined)
                record[x] = attrs[x];
        }
        return true;
    };

    if (typeof(records) === 'object')
        tbl.setRecords(records, idAttr);

    tbl.followTable = function(targetTable, tag, filter, attrname_attrnames){
        if (targetTable == null || targetTable['_type'] !== 'table')
            return;

        if (filter != null){
            this.setEvent(tag + '_Filter', table.event('beforeAdd', filter, 9999));

            let attrs = attrname_attrnames instanceof Array ? attrname_attrnames : [attrname_attrnames];
            let msgs = [];
            for (let i = 0; i < attrs.length; i++)
                msgs.push('afterAttr_' + attrs[i]);
            targetTable.setEvent(tag + '_FollowUpdate',
                table.event(msgs,
                    (table, id, record) => { tbl.setWithoutMessage(id, filter(table, id, record) ? record : null); return true; },
                    9999));
        }
        targetTable.setEvent(tag + '_FollowAdd',
            table.event('afterAdd',
                (table, id, record) => { tbl(id, record); return true; },
                9999));
        targetTable.setEvent(tag + '_FollowDel',
            table.event('afterDel',
                (table, id, record) => { tbl(id, null); return true; },
                9999));

        targetTable.doWhile((table, id) => { tbl(id, table(id)); return true; });
    };

    tbl.cancelFollowTable = function(targetTable, tag){
        this.delEvent(tag + '_Filter');
        targetTable.delEvent(tag + '_FollowAdd');
        targetTable.delEvent(tag + '_FollowDel');
        targetTable.delEvent(tag + '_FollowUpdate');
    };

    tbl._defaultSelection = table.tableSelection(tbl);

    tbl.where = function(filter){
        return table.tableSelection(this, filter);
    };

    tbl.togetherWith = function(...args){ return this._defaultSelection.togetherWith(...args); };

    tbl.doWhile = function(cb){ this._defaultSelection.doWhile(cb); };

    tbl.count = function(){ return this._defaultSelection.count(); };

    /**
     * update attributes, two usage :
     *     1. update(id, attrs_attrscb) to update one-record;
     *     2. update(attrs_attrscb) to update all-record;
     * attrs_attrscb can be attributes, OR attributes-callback as (table, id) => {}
     * @param id_attrs_attrscb (required) id OR attributes OR attributes-callback
     * @param attrs_attrscb (option) attributes OR attributes-callback, if pre-param is id, then it is required
     */
    tbl.update = function(id_attrs_attrscb, attrs_attrscb){
        return attrs_attrscb === undefined ? this._defaultSelection.update(id_attrs_attrscb) : this._update(id_attrs_attrscb, attrs_attrscb);
    };

    tbl.updateWithoutMessage = function(id_attrs_attrscb, attrs_attrscb){
        return attrs_attrscb === undefined ? this._defaultSelection.updateWithoutMessage(id_attrs_attrscb) : this._updateWithoutMessage(id_attrs_attrscb, attrs_attrscb);
    };

    tbl.first = function(){ return this._defaultSelection.first(); };

    tbl.randomOne = function(){ return this._defaultSelection.randomOne(); };

    tbl.randomSome = function(num){ return this._defaultSelection.randomSome(num); };

    tbl.any = function(cb){ return this._defaultSelection.any(cb); };

    tbl.all = function(cb){ return this._defaultSelection.all(cb); };

    tbl.sum = function(cb){ return this._defaultSelection.sum(cb); };

    tbl.product = function(cb){ return this._defaultSelection.product(cb); };

    tbl.min = function(cb){ return this._defaultSelection.min(cb); };

    tbl.max = function(cb){ return this._defaultSelection.max(cb); };

    tbl.concatString = function(cb, connector){ return this._defaultSelection.concatString(cb, connector); };

    tbl.concatArray = function(cb){ return this._defaultSelection.concatArray(cb); };

    tbl.extractTable = function(cb, idAttr){ return this._defaultSelection.extractTable(cb, idAttr); };

    return tbl;
};

/**
 * Return new Event-class entity, see table.setEvent().
 * @param msg_msgs (required) message OR array of listen messages, table's message including: 'beforeAdd','beforeDel','afterAdd','afterDel','afterAttr_'+attributeName,'beforeAttr_'+attributeName
 * @param callback (option, default:() => true) event's callback:(table, id, record, eventId)=>{}. return false to break events-loop
 * @param tOrder (option, default:0) event call time-order, in one message, all relative events are called one-by-one ordered by tOrder
 * @constructor
 */
table.event = function(msg_msgs, callback, tOrder){
    callback = callback instanceof Function ? callback : () => true;
    var evt = function(table, id, record, eventId, ...args){
        return callback(table, id, record, eventId, ...args);
    };
    evt._type = 'event';
    evt.msgs = msg_msgs instanceof Array ? new Set(msg_msgs) : new Set([msg_msgs]);
    evt.tOrder = typeof(tOrder) === 'number' ? tOrder : 0;
    return evt;
};

/**
 * Return new Selection of table to do callback-loop, see table.where()
 * @param table_selection (required) target table OR tableSelection
 * @param filter (option, default:() => true), filter's callback: (table,id) => {}， return true when select this record
 * @constructor
 */
table.tableSelection = function(table_selection, filter){

    filter = filter instanceof Function ? filter : () => true;

    /**
     * Do callback to select-records loop, if callback return false, loop breaks
     * @param cb (required) loop-callback: (table,id) => {}, return false to break the loop
     */
    var ts = function(cb){
        for (let id in ts._records){
            if (id !== undefined && ts._filter(ts._table,id)){
                if (!cb(ts._table,id))
                    break;
            }
        }
    };

    ts._type = 'tableSelection';
    if (table_selection['_type'] === 'table'){
        ts._table = table_selection;
        ts._filter = filter;
        ts._records = table_selection._records;
    }else if (table_selection['_type'] === 'tableSelection'){
        ts._table = table_selection._table;
        ts._filter = (table, id) => table_selection._filter(table, id) && filter(table, id);
        ts._records = table_selection._records;
    }else{
        return null;
    }

    ts.doWhile = function(cb){
        this(cb);
    };

    ts.where = function(filter){
        return table.tableSelection(this, filter);
    };

    ts.togetherWith = function(...args){
        return table.comboSelection(this, ...args);
    };

    ts.count = function(){
        let c = 0;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
            }
        }
        return c;
    };

    ts.update = function(attrs_attrscb){
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                this._table._update(id, attrs_attrscb);
            }
        }
    };

    ts.updateWithoutMessage = function(attrs_attrscb){
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id))
                this._table._updateWithoutMessage(id, attrs_attrscb);
        }
    };

    ts.first = function(){
        let c = 0;
        let v = {table:this._table,id:undefined};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v.id = id;
                break;
            }
        }
        return {cursor:c,val:v};
    };

    ts.randomOne = function(){
        let count = this.count();
        let rand = Math.ceil(Math.random()*count);
        let c = 0;
        let v = {table:this._table,id:undefined};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (c == rand){
                    v.id = id;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    ts.randomSome = function(num){
        let count = this.count();
        num = num <= count ? num : count;
        let rands = new Set();
        while (rands.size < num){
            let rand = Math.ceil(Math.random()*count);
            if (!rands.has(rand))
                rands.add(rand);
        }
        let c = 0;
        let v = [];
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (rands.has(c))
                    v.push({table:this._table,id:id});
            }
        }
        return {cursor:c,val:v};
    };

    ts.any = function(cb){
        let c = 0;
        let v = false;
        cb = cb ? cb : (id,table) => true;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (cb(this._table,id)){
                    v = true;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    ts.all = function(cb){
        let c = 0;
        let v = true;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (!cb(this._table,id)){
                    v = false;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    ts.sum = function(cb){
        let c = 0;
        let v = 0;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v += cb(this._table,id);
            }
        }
        return {cursor:c,val:v};
    };

    ts.product = function(cb){
        let c = 0;
        let v = 1;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v *= cb(this._table,id);
                if (v == 0)
                    break;
            }
        }
        return {cursor:c,val:v};
    };

    ts.min = function(cb){
        let c = 0;
        let v = {table:this._table,id:undefined,minValue:Number.MAX_VALUE};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                let val = cb(this._table,id);
                if (v.minValue > val){
                    v.id = id;
                    v.minValue = val;
                }
            }
        }
        return {cursor:c,val:v};
    };

    ts.max = function(cb){
        let c = 0;
        let v = {table:this._table,id:undefined,maxValue:Number.MIN_VALUE};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                let val = cb(this._table,id);
                if (v.maxValue < val){
                    v.id = id;
                    v.maxValue = val;
                }
            }
        }
        return {cursor:c,val:v};
    };

    ts.concatString = function(cb, connector){
        connector = connector ? connector : '';
        let c = 0;
        let v = '';
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v = v + connector + cb(this._table,id);
            }
        }
        v = v.substr(connector.length);
        return {cursor:c,val:v};
    };

    ts.concatArray = function(cb){
        let c = 0;
        let v = [];
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v.push(cb(this._table,id));
            }
        }
        return {cursor:c,val:v};
    };

    ts.extractTable = function(cb, idAttr){
        let v = {};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                let rtn = cb(this._table,id);
                if (rtn !== undefined)
                    v[id] = rtn;
            }
        }
        return table(v, idAttr);
    };

    return ts;
};

/**
 * Return new Combo Selection of several tableSelections to do callback-loop, see tableSelection.togetherWith()
 * @param ...args (required) target tables OR tableSelections OR comboSelections
 * @constructor
 */
table.comboSelection = function(...args) {

    /**
     * Do callback to select-records loop, if callback return false, loop breaks
     * @param cb (required) loop-callback: (table,id) => {}, return false to break the loop
     */
    var cs = function(cb){
        for (let selid in cs._selections){
            if (selid !== undefined){
                let selection = cs._selections[selid];
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        if (!cb(selection._table,id))
                            return;
                    }
                }
            }
        }
    };

    cs._type = 'comboSelection';
    cs._selections = [];

    for (let i = 0; i < args.length; i++){
        let arg = args[i];
        switch (arg['_type']){
            case 'table':
                cs._selections.push(arg._defaultSelection);
                break;
            case 'tableSelection':
                cs._selections.push(arg);
                break;
            case 'comboSelection':
                cs._selections = cs._selections.concat(arg._selections);
                break;
            default:
                return null;
        }
    }

    cs.doWhile = function(cb){
        return this(cb);
    };

    cs.where = function(filter){
        let selections = this._selections;
        for (let selid = 0; selid < selections.length; selid++)
            selections[selid] = selections[selid].where(filter);
        return table.comboSelection(...selections);
    };

    cs.togetherWith = function(...args){
        return table.comboSelection(...this._selections, ...args);
    };

    cs.count = function(){
        let c = 0;
        for (let selid in this._selections){
            if (selid !== undefined)
                c += this._selections[selid].count();
        }
        return c;
    };

    cs.update = function(attrs_attrscb){
        for (let selid in this._selections){
            if (selid !== undefined)
                this._selections[selid].update(attrs_attrscb);
        }
    };

    cs.updateWithoutMessage = function(attrs_attrscb){
        for (let selid in this._selections){
            if (selid !== undefined)
                this._selections[selid].updateWithoutMessage(attrs_attrscb);
        }
    };

    cs.first = function(){
        let c = 0;
        let v = {table:null,id:null};
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].first();
                if (r.val != null && r.val.id != undefined){
                    c += r.cursor;
                    v = r.val;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    cs.randomOne = function(){
        let count = this.count();
        let rand = Math.ceil(Math.random()*count);
        let c = 0;
        let v = {table:undefined,id:undefined};
        for (let selid in this._selections){
            if (selid !== undefined){
                let selection = this._selections[selid]
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        c++;
                        if (c == rand){
                            v.table = selection._table;
                            v.id = id;
                            break;
                        }
                    }
                }
            }
        }

        return {cursor:c,val:v};
    };

    cs.randomSome = function(num){
        let count = this.count();
        num = num <= count ? num : count;
        let rands = new Set();
        while (rands.size < num){
            let rand = Math.ceil(Math.random()*count);
            if (!rands.has(rand))
                rands.add(rand);
        }

        let c = 0;
        let v = [];
        for (let selid in this._selections){
            if (selid !== undefined){
                var selection = this._selections[selid];
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        c++;
                        if (rands.has(c)){
                            v.push({table:selection._table,id:id});
                        }
                    }
                }
            }
        }
        return {cursor:c,val:v};
    };

    cs.any = function(cb){
        let c = 0;
        let v = false;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].any(cb);
                c += r.cursor;
                if (r.val === true){
                    v = r.val;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    cs.all = function(cb){
        let c = 0;
        let v = true;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].all(cb);
                c += r.cursor;
                if (r.val === false){
                    v = false;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    cs.sum = function(cb){
        let c = 0;
        let v = 0;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].sum(cb);
                c += r.cursor;
                v += r.val;
            }
        }
        return {cursor:c,val:v};
    };

    cs.product = function(cb){
        let c = 0;
        let v = 1;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].product(cb);
                c += r.cursor;
                v *= r.val;
                if (v == 0)
                    break;
            }
        }
        return {cursor:c,val:v};
    };

    cs.min = function(cb){
        let c = 0;
        let v = {table:null,id:undefined,minValue:Number.MAX_VALUE};
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].min(cb);
                c += r.cursor;
                if (r.val != null && r.val.id !== undefined){
                    if (v.minValue > r.minValue){
                        v.table = r.table;
                        v.id = r.id;
                        v.minValue = r.minValue;
                    }
                }
            }
        }
        return {cursor:c,val:v};
    };

    cs.max = function(cb){
        let c = 0;
        let v = {table:null,id:undefined,maxValue:Number.MIN_VALUE};
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].max(cb);
                c += r.cursor;
                if (r.val != null && r.val.id !== undefined){
                    if (v.maxValue < r.maxValue){
                        v.table = r.table;
                        v.id = r.id;
                        v.maxValue = r.maxValue;
                    }
                }
            }
        }
        return {cursor:c,val:v};
    };

    cs.concatString = function(cb, connector){
        connector = connector ? connector : '';
        let c = 0;
        let v = '';
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].concatString(cb, connector);
                c += r.cursor;
                v = v + connector + r.val;
            }
        }
        v = v.substr(connector.length);
        return {cursor:c,val:v};
    };

    cs.concatArray = function(cb){
        let c = 0;
        let v = [];
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].concatArray(cb);
                c += r.cursor;
                v = v.concat(r.val);
            }
        }
        return {cursor:c,val:v};
    };

    cs.extractTable = function(cb, idAttr){
        let v = {};
        for (let selid in this._selections){
            if (selid !== undefined){
                let selection = this._selections[selid];
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        let rtn = cb(selection._table,id);
                        if (rtn !== undefined)
                            v['' + selid + '_' + id] = rtn;
                    }
                }
            }
        }
        return table(v, idAttr);
    };
};

if (module)
    module.exports = table;